#!/bin/sh

# Copyright (C) 2014-2025 Internet Systems Consortium, Inc. ("ISC")
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# shellcheck disable=SC2034
# SC2034: ... appears unused. Verify use (or export if used externally).

# shellcheck disable=SC2153
# SC2153: Possible misspelling: ... may not be assigned, but ... is.

# shellcheck disable=SC2154
# SC2154: bin_path is referenced but not assigned.

# Exit with error if commands exit with non-zero and if undefined variables are
# used.
set -eu

# Include XML reporting library.
# shellcheck source=src/lib/testutils/xml_reporting_test_lib.sh.in
. "@abs_top_builddir@/src/lib/testutils/xml_reporting_test_lib.sh"

prefix="@prefix@"

# Expected version
VERSION='@VERSION@'
EXTENDED_VERSION='@EXTENDED_VERSION@'

# Kea environment variables for shell tests.
# KEA_LOGGER_DESTINATION is set per test with set_logger.
export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc"
export KEA_LOCKFILE_DIR="@abs_top_builddir@/test_lockfile_dir"
export KEA_PIDFILE_DIR="@abs_top_builddir@/test_pidfile_dir"
KEA_DHCP4_LOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/load_marker.txt"
KEA_DHCP4_UNLOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/unload_marker.txt"
KEA_DHCP4_SRV_CONFIG_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/srv_config_marker_file.txt"
KEA_DHCP6_LOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/load_marker.txt"
KEA_DHCP6_UNLOAD_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/unload_marker.txt"
KEA_DHCP6_SRV_CONFIG_MARKER_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/srv_config_marker_file.txt"

# A list of Kea processes, mainly used by the cleanup functions.
KEA_PROCS="kea-dhcp4 kea-dhcp6 kea-dhcp-ddns kea-ctrl-agent"

### Colors ###

if test -t 1; then
  green='\033[92m'
  red='\033[91m'
  reset='\033[0m'
fi

### Logging functions ###

# Prints error message.
test_lib_error() {
    local s="${1-}"             # Error message.
    local no_new_line="${2-}"   # If specified, the message is not terminated
                                # with new line.
    printf "ERROR/test_lib: %s" "${s}"
    if [ -z "${no_new_line}" ]; then
        printf '\n'
    fi
}

# Prints info message.
test_lib_info() {
    local s="${1-}"             # Info message.
    local no_new_line="${2-}"   # If specified, the message is not terminated
                                # with new line.
    printf "INFO/test_lib: %s" "${s}"
    if [ -z "${no_new_line}" ]; then
        printf '\n'
    fi
}

### Assertions ###

# Assertion that checks if two numbers are equal.
# If numbers are not equal, the mismatched values are presented and the
# detailed error is printed. The detailed error must use the printf
# formatting like this:
#    "Expected that some value 1 %d is equal to some other value %d".
assert_eq() {
    val1=${1}           # Reference value
    val2=${2}           # Tested value
    detailed_err=${3-}  # Optional detailed error format string
    # If nothing found, present an error an exit.
    if [ "${val1}" -ne "${val2}" ]; then
        printf 'Assertion failure: %s != %s, expected %s, got %s\n' \
            "${val1}" "${val2}" "${val1}" "${val2}"
        # shellcheck disable=SC2059
        # SC2059: Don't use variables in the printf format string. Use printf '..%s..' "$foo"
        ERROR=$(printf "${detailed_err}" "${val1}" "${val2}")
        printf '%s\n%s\n' "${ERROR}" "${OUTPUT}" >&2
        clean_exit 1
    fi
}

# Assertion that checks that two strings are equal.
# If strings are not equal, the mismatched values are presented and the
# detailed error is printed. The detailed error must use the printf
# formatting like this:
#    "Expected that some value 1 %d is equal to some other value %d".
assert_str_eq() {
    val1=${1}           # Reference value
    val2=${2}           # Tested value
    detailed_err=${3-}  # Optional detailed error format string
    # If nothing found, present an error an exit.
    if [ "${val1}" != "${val2}" ]; then
        printf 'Assertion failure: %s != %s, expected "%s", got "%s"\n' \
            "${val1}" "${val2}" "${val1}" "${val2}"
        # shellcheck disable=SC2059
        # SC2059: SC2059: Don't use variables in the printf format string. Use printf '..%s..' "$foo".
        ERROR=$(printf "${detailed_err}" "${val1}" "${val2}")
        printf '%s\n%s\n' "${ERROR}" "${OUTPUT}" >&2
        clean_exit 1
    fi
}

# Assertion that checks that two strings are NOT equal.
# If strings are equal, the mismatched values are presented and the
# optional detailed error, if any, is printed.
assert_str_neq() {
    reference=${1}        # Reference value
    tested=${2}           # Tested value
    detailed_error=${3-}  # Optional detailed error format string
    if test "${reference}" = "${tested}"; then
        printf 'Assertion failure: expected different strings, but '
        printf 'both variables have the value "%s".\n' "${reference}"
        printf '%s\n%s\n' "${detailed_error}" "${OUTPUT}" >&2
        clean_exit 1
    fi
}

# Assertion that checks if one string contains another string.
# If assertion fails, both strings are displayed and the detailed
# error is printed. The detailed error must use the printf formatting
# like this:
#    "Expected some string to contain this string: %s".
assert_string_contains() {
    pattern="${1}"      # Substring or awk pattern
    text="${2}"         # Text to be searched for substring
    detailed_err="${3}" # Detailed error format string
    # Search for a pattern
    match=$( printf "%s" "${text}" | awk /"${pattern}"/ )
    # If nothing found, present an error and exit.
    if [ -z "${match}" ]; then
        ERROR=$(printf \
"Assertion failure:
\"%s\"

does not contain pattern:
\"%s\"

${detailed_err}
" "${text}" "${pattern}" "${pattern}")
        printf '%s\n%s\n' "${ERROR}" "${OUTPUT}" >&2
        clean_exit 1
    fi
}

# Runs all the given arguments as a single command. Maintains quoting. Places
# output in ${OUTPUT} and exit code in ${EXIT_CODE}. Does not support pipes and
# redirections. Support for them could be added through eval and single
# parameter assignment, but eval is not recommended.
# shellcheck disable=SC2034
# SC2034: ... appears unused. Verify use (or export if used externally).
run_command() {
    if test -n "${DEBUG+x}"; then
        printf '%s\n' "${*}" >&2
    fi
    set +e
    OUTPUT=$("${@}")
    EXIT_CODE=${?}
    set -e
}

# Enable traps to print FAILED status when a command fails unexpectedly or when
# the user sends a SIGINT. Used in `test_start`.
traps_on() {
    for t in HUP INT QUIT KILL TERM EXIT; do
        # shellcheck disable=SC2064
        # SC2064: Use single quotes, otherwise this expands now rather than when signalled.
        # reason: we want ${red-} and ${reset-} to expand here, at trap-time
        # they will be empty or have other values
        trap "
            exit_code=\${?}
            printf '${red-}[  FAILED  ]${reset-} %s (exit code: %d)\n' \
                \"\${TEST_NAME}\" \"\${exit_code}\"
        " "${t}"
    done
}

# Disable traps so that a double status is not printed. Used in `test_finish`
# after the status has been printed explicitly.
traps_off() {
    for t in HUP INT QUIT KILL TERM EXIT; do
        trap - "${t}"
    done
}

# Print UNIX time with millisecond resolution.
get_current_time() {
    local time
    time=$(date +%s%3N)

    # In some systems, particularly BSD-based, `+%3N` millisecond resolution is
    # not supported. It instead prints the literal '3N', but we check for any
    # alphabetical character. If we do find one, revert to second resolution and
    # convert to milliseconds.
    if printf '%s' "${time}" | grep -E '[A-Za-z]' > /dev/null 2>&1; then
        time=$(date +%s)
        time=$((1000 * time))
    fi

    printf '%s' "${time}"
}

# Begins a test by printing its name.
test_start() {
    TEST_NAME=${1-}
    if [ -z "${TEST_NAME}" ]; then
        test_lib_error "test_start requires test name as an argument"
        clean_exit 1
    fi

    # Set traps first to fail if something goes wrong.
    traps_on

    # Announce test start.
    printf "${green-}[ RUN      ]${reset-} %s\n" "${TEST_NAME}"

    # Remove dangling Kea instances and remove log files.
    cleanup

    # Make sure lockfile and pidfile directories exist. They are used in some
    # tests.
    mkdir -p "${KEA_LOCKFILE_DIR}"
    # There are certain tests that intentionally run without a KEA_PIDFILE_DIR
    # e.g. keactrl.status_test. Only create the directory if the test requires
    # one.
    if test -n "${KEA_PIDFILE_DIR+x}"; then
      mkdir -p "${KEA_PIDFILE_DIR}"
    fi

    # Start timer in milliseconds.
    START_TIME=$(get_current_time)
}

# Prints test result an cleans up after the test.
test_finish() {
    # Exit code to be returned by the exit function
    local exit_code="${1}"

    # Stop timer and set duration.
    FINISH_TIME=$(get_current_time)
    local duration
    duration=$((FINISH_TIME - START_TIME))

    # Add the test result to the XML.
    report_test_result_in_xml "${TEST_NAME}" "${exit_code}" "${duration}"

    if [ "${exit_code}" -eq 0 ]; then
        printf "${green-}[       OK ]${reset-} %s\n" "${TEST_NAME}"
    else
        # Dump log file for debugging purposes if specified and exists.
        # Otherwise the code below would simply call cat.
        # Use ${var+x} to test if ${var} is defined.
        if test -n "${LOG_FILE+x}" && test -s "${LOG_FILE}"; then
            printf 'Log file dump:\n'
            cat "${LOG_FILE}"
        fi
        printf "${red-}[  FAILED  ]${reset-} %s\n" "${TEST_NAME}"
    fi

    # Remove dangling Kea instances and log files.
    cleanup

    # Reset traps.
    traps_off

    # Explicitly return ${exit_code}. The effect should be for `meson test` to
    # return with the exit same code or at least another non-zero exit code thus
    # reporting a failure.
    return "${exit_code}"
}

# Stores the configuration specified as a parameter in the configuration
# file which name has been set in the ${CFG_FILE} variable.
create_config() {
    local cfg="${1-}"  # Configuration string.
    if [ -z "${CFG_FILE+x}" ]; then
        test_lib_error "create_config requires CFG_FILE variable be set"
        clean_exit 1

    elif [ -z "${cfg}" ]; then
        test_lib_error "create_config requires argument holding a configuration"
        clean_exit 1
    fi
    printf 'Creating Kea configuration file: %s.\n' "${CFG_FILE}"
    printf '%b' "${cfg}" > "${CFG_FILE}"
}

# Stores the DHCP4 configuration specified as a parameter in the
# configuration file which name has been set in the ${DHCP4_CFG_FILE}
# variable.
create_dhcp4_config() {
    local cfg="${1-}"  # Configuration string.
    if [ -z "${DHCP4_CFG_FILE+x}" ]; then
        test_lib_error "create_dhcp4_config requires DHCP4_CFG_FILE \
variable be set"
        clean_exit 1

    elif [ -z "${cfg}" ]; then
        test_lib_error "create_dhcp4_config requires argument holding a \
configuration"
        clean_exit 1
    fi
    printf 'Creating Dhcp4 configuration file: %s.\n' "${DHCP4_CFG_FILE}"
    printf '%b' "${cfg}" > "${DHCP4_CFG_FILE}"
}

# Stores the DHCP6 configuration specified as a parameter in the
# configuration file which name has been set in the ${DHCP6_CFG_FILE}
# variable.
create_dhcp6_config() {
    local cfg="${1-}"  # Configuration string.
    if [ -z "${DHCP6_CFG_FILE+x}" ]; then
        test_lib_error "create_dhcp6_config requires DHCP6_CFG_FILE \
variable be set"
        clean_exit 1

    elif [ -z "${cfg}" ]; then
        test_lib_error "create_dhcp6_config requires argument holding a \
configuration"
        clean_exit 1
    fi
    printf 'Creating Dhcp6 configuration file: %s.\n' "${DHCP6_CFG_FILE}"
    printf '%b' "${cfg}" > "${DHCP6_CFG_FILE}"
}

# Stores the D2 configuration specified as a parameter in the
# configuration file which name has been set in the ${D2_CFG_FILE}
# variable.
create_d2_config() {
    local cfg="${1-}"  # Configuration string.
    if [ -z "${D2_CFG_FILE+x}" ]; then
        test_lib_error "create_d2_config requires D2_CFG_FILE \
variable be set"
        clean_exit 1

    elif [ -z "${cfg}" ]; then
        test_lib_error "create_d2_config requires argument holding a \
configuration"
        clean_exit 1
    fi
    printf 'Creating D2 configuration file: %s.\n' "${D2_CFG_FILE}"
    printf '%b' "${cfg}" > "${D2_CFG_FILE}"
}

# Stores the CA configuration specified as a parameter in the
# configuration file which name has been set in the ${CA_CFG_FILE}
# variable.
create_ca_config() {
    local cfg="${1-}"  # Configuration string.
    if [ -z "${CA_CFG_FILE+x}" ]; then
        test_lib_error "create_ca_config requires CA_CFG_FILE \
variable be set"
        clean_exit 1

    elif [ -z "${cfg}" ]; then
        test_lib_error "create_ca_config requires argument holding a \
configuration"
        clean_exit 1
    fi
    printf 'Creating Ca configuration file: %s.\n' "${CA_CFG_FILE}"
    printf '%b' "${cfg}" > "${CA_CFG_FILE}"
}

# Stores the NC configuration specified as a parameter in the
# configuration file which name has been set in the ${NC_CFG_FILE}
# variable.
create_nc_config() {
    local cfg="${1-}"  # Configuration string.
    if [ -z "${NC_CFG_FILE+x}" ]; then
        test_lib_error "create_nc_config requires NC_CFG_FILE \
variable be set"
        clean_exit 1

    elif [ -z "${cfg}" ]; then
        test_lib_error "create_nc_config requires argument holding a \
configuration"
        clean_exit 1
    fi
    printf 'Creating Nc configuration file: %s.\n' "${NC_CFG_FILE}"
    printf '%b' "${cfg}" > "${NC_CFG_FILE}"
}

# Stores the keactrl configuration specified as a parameter in the
# configuration file which name has been set in the ${KEACTRL_CFG_FILE}
# variable.
create_keactrl_config() {
    local cfg="${1-}" # Configuration string.
    if [ -z "${KEACTRL_CFG_FILE+x}" ]; then
        test_lib_error "create_keactrl_config requires KEACTRL_CFG_FILE \
variable be set"
        clean_exit 1

    elif [ -z "${cfg}" ]; then
        test_lib_error "create_keactrl_config requires argument holding a \
configuration"
        clean_exit 1
    fi
    printf 'Creating keactrl configuration file: %s.\n' "${KEACTRL_CFG_FILE}"
    printf '%b' "${cfg}" > "${KEACTRL_CFG_FILE}"
}

# Sets Kea logger to write to the file specified by the global value
# ${LOG_FILE}.
set_logger() {
    if [ -z "${LOG_FILE+x}" ]; then
        test_lib_error "set_logger requires LOG_FILE variable be set"
        clean_exit 1
    fi
    printf 'Kea log will be stored in %s.\n' "${LOG_FILE}"
    export KEA_LOGGER_DESTINATION="${LOG_FILE}"
}

# Checks if specified process is running.
#
# This function uses PID file to obtain the PID and then calls
# 'kill -0 <pid>' to check if the process is alive.
# The PID files are expected to be located in the ${KEA_PIDFILE_DIR},
# and their names should match the following pattern:
# <cfg_file_name>.<proc_name>.pid. If the <cfg_file_name> is not
# specified a 'test_config' is used by default.
#
# Return value:
#   _GET_PID: holds a PID if process is running
#   _GET_PIDS_NUM: holds 1 if process is running, 0 otherwise
get_pid() {
    local proc_name="${1-}"         # Process name
    local cfg_file_name="${2-}"     # Configuration file name without extension.

    # Reset PID results.
    _GET_PID=0
    _GET_PIDS_NUM=0

    # PID file name includes process name. The process name is required.
    if [ -z "${proc_name}" ]; then
        test_lib_error "get_pid requires process name"
        clean_exit 1
    fi

    # There are certain tests that intentionally run without a KEA_PIDFILE_DIR
    # e.g. keactrl.status_test. We can't get the PID if KEA_PIDFILE_DIR is not
    # defined. In this case, this function is reporting process not running
    # (_GET_PID == 0).
    if test -z "${KEA_PIDFILE_DIR+x}"; then
      return
    fi

    # PID file name includes server configuration file name. For most of
    # the tests it is 'test-config' (excluding .json extension). It is
    # possible to specify custom name if required.
    if [ -z "${cfg_file_name}" ]; then
        cfg_file_name="test_config"
    fi

    # Get the absolute location of the PID file for the specified process
    # name.
    abs_pidfile_path="${KEA_PIDFILE_DIR}/${cfg_file_name}.${proc_name}.pid"

    # If the PID file exists, get the PID and see if the process is alive.
    pid=$(cat "${abs_pidfile_path}" 2> /dev/null || true)
    if test -n "${pid}"; then
        if kill -0 "${pid}" > /dev/null 2>&1; then
            _GET_PID=${pid}
            _GET_PIDS_NUM=1
        fi
    fi
}

# Get the name of the process identified by PID.
get_process_name() {
    local pid="${1-}"
    if test -z "${pid}"; then
        test_lib_error 'expected PID parameter in get_process_name'
        clean_exit 1
    fi

    ps "${pid}" | tr -s ' ' | cut -d ' ' -f 6- | head -n 2 | tail -n 1
}

# Wait for file to be created.
wait_for_file() {
    local file="${1-}"
    if test -z "${file}"; then
        test_lib_error 'expected file parameter in wait_for_file'
        clean_exit 1
    fi

    local timeout='4' # seconds
    local deadline="$(($(date +%s) + timeout))"
    while ! test -f "${file}"; do
        if test "${deadline}" -lt "$(date +%s)"; then
            # Time is up.
            printf 'ERROR: file "%s" was not created in time.\n' "${file}" >&2
            return 1
        fi
        printf 'Waiting for file "%s" to be created...\n' "${file}"
        sleep 1
    done
}

# Wait for process identified by PID to die.
wait_for_process_to_stop() {
    local pid="${1-}"
    if test -z "${pid}"; then
        test_lib_error 'expected PID parameter in wait_for_process_to_stop'
        clean_exit 1
    fi

    local timeout='4' # seconds
    local deadline="$(($(date +%s) + timeout))"
    while ps "${pid}" >/dev/null; do
        if test "${deadline}" -lt "$(date +%s)"; then
            # Time is up.
            printf 'ERROR: %s is not stopping.\n' "$(get_process_name "${pid}")" >&2
            return 1
        fi
        printf 'Waiting for %s to stop...\n' "$(get_process_name "${pid}")"
        sleep 1
    done
}

# Kills processes specified by name.
#
# This function kills all processes having a specified name.
# It uses 'pgrep' to obtain pids of those processes.
# This function should be used when identifying process by
# the value in its PID file is not relevant.
#
# Linux limitation for pgrep: The process name used for matching is
# limited to the 15 characters. If you call this with long process
# names, add this before pgrep:
# proc_name=$(printf '%s' "${proc_name}" | cut -c1-15)
kill_processes_by_name() {
    local proc_name="${1-}"     # Process name
    if [ -z "${proc_name}" ]; then
        test_lib_error "kill_processes_by_name requires process name"
        clean_exit 1
    fi

    # Obtain PIDs of running processes.
    local pids
    pids=$(pgrep "${proc_name}" || true)
    # For each PID found, send kill signal.
    for pid in ${pids}; do
        printf 'Shutting down Kea process %s with PID %d...\n' "${proc_name}" "${pid}"
        kill -9 "${pid}" || true
    done

    # Wait for all processes to stop.
    for pid in ${pids}; do
        printf 'Waiting for Kea process %s with PID %d to stop...\n' "${proc_name}" "${pid}"
        wait_for_process_to_stop "${pid}"
    done
}

# Returns the number of occurrences of the Kea log message in the log file.
# Return value:
#   _GET_LOG_MESSAGES: number of log message occurrences.
get_log_messages() {
    local msg="${1}"  # Message id, e.g. DHCP6_SHUTDOWN
    if [ -z "${msg}" ]; then
        test_lib_error "get_log_messages require message identifier"
        clean_exit 1
    fi
    _GET_LOG_MESSAGES=0
    # If log file is not present, the number of occurrences is 0.
    # Use ${var+x} to test if ${var} is defined.
    if test -n "${LOG_FILE+x}" && test -s "${LOG_FILE}"; then
        # Grep log file for the logger message occurrences and remove
        # whitespaces, if any.
        _GET_LOG_MESSAGES=$(grep -Fo "${msg}" "${LOG_FILE}" | wc -w | tr -d " ")
    fi
}

# Returns the number of server configurations performed so far. Also
# returns the number of configuration errors.
# Return values:
#   _GET_RECONFIGS: number of configurations so far.
#   _GET_RECONFIG_ERRORS: number of configuration errors.
get_reconfigs() {
    # Grep log file for CONFIG_COMPLETE occurrences. There should
    # be one occurrence per (re)configuration.
    _GET_RECONFIGS=$(grep -Fo CONFIG_COMPLETE "${LOG_FILE}" | wc -w)
    # Grep log file for CONFIG_LOAD_FAIL to check for configuration
    # failures.
    _GET_RECONFIG_ERRORS=$(grep -Fo CONFIG_LOAD_FAIL "${LOG_FILE}" | wc -w)
    # Remove whitespaces
    ${_GET_RECONFIGS##*[! ]}
    ${_GET_RECONFIG_ERRORS##*[! ]}
}

# Remove the given directories or files if they exist.
remove_if_exists() {
    while test ${#} -gt 0; do
        if test -e "${1}"; then
            rm -rf "${1}"
        fi
        shift
    done
}

# Performs cleanup after test.
# It shuts down running Kea processes and removes temporary files.
# The location of the log file and the configuration files should be set
# in the ${LOG_FILE}, ${CFG_FILE} and ${KEACTRL_CFG_FILE} variables
# respectively, prior to calling this function.
cleanup() {
    # If there is no KEA_PROCS set, just return
    if [ -z "${KEA_PROCS}" ]; then
        return
    fi

    # KEA_PROCS holds the name of all Kea processes. Shut down each
    # of them if running.
    for proc_name in ${KEA_PROCS}
    do
        get_pid "${proc_name}"
        # Shut down running Kea process.
        if [ "${_GET_PIDS_NUM}" -ne 0 ]; then
            printf 'Shutting down Kea process having pid %d.\n' "${_GET_PID}"
            kill -9 "${_GET_PID}"
        fi
    done

    # Kill any running LFC processes. Even though 'kea-lfc' creates PID
    # file we rather want to use 'pgrep' to find the process PID, because
    # kea-lfc execution is not controlled from the test and thus there
    # is possibility that process is already/still running but the PID
    # file doesn't exist for it. As a result, the process will not
    # be killed. This is not a problem for other processes because
    # tests control launching them and monitor when they are shut down.
    kill_processes_by_name "kea-lfc"

    # Remove temporary files.
    remove_if_exists \
        "${CA_CFG_FILE-}" \
        "${CFG_FILE-}" \
        "${D2_CFG_FILE-}" \
        "${DHCP4_CFG_FILE-}" \
        "${KEA_DHCP4_LOAD_MARKER_FILE-}" \
        "${KEA_DHCP4_UNLOAD_MARKER_FILE-}" \
        "${KEA_DHCP4_SRV_CONFIG_MARKER_FILE-}" \
        "${DHCP6_CFG_FILE-}" \
        "${KEA_DHCP6_LOAD_MARKER_FILE-}" \
        "${KEA_DHCP6_UNLOAD_MARKER_FILE-}" \
        "${KEA_DHCP6_SRV_CONFIG_MARKER_FILE-}" \
        "${KEACTRL_CFG_FILE-}" \
        "${KEA_LOCKFILE_DIR-}" \
        "${KEA_PIDFILE_DIR-}" \
        "${NC_CFG_FILE-}"

    # Use ${var+x} to test if ${var} is defined.
    if test -n "${LOG_FILE+x}" && test -n "${LOG_FILE}"; then
        rm -rf "${LOG_FILE}"
        rm -rf "${LOG_FILE}.lock"
    fi
    # Use asterisk to remove all files starting with the given name,
    # in case the LFC has been run. LFC creates files with postfixes
    # appended to the lease file name.
    if test -n "${LEASE_FILE+x}" && test -n "${LEASE_FILE}"; then
        rm -rf "${LEASE_FILE}"*
    fi
}

# Exists the test in the clean way.
# It performs the cleanup and prints whether the test has passed or failed.
# If a test fails, the Kea log is dumped.
clean_exit() {
    exit_code=${1-}  # Exit code to be returned by the exit function.
    case ${exit_code} in
        ''|*[!0-9]*)
            test_lib_error "argument passed to clean_exit must be a number" ;;
    esac
    # Print test result and perform a cleanup
    test_finish "${exit_code}"
    exit "${exit_code}"
}

# Starts Kea process in background using a configuration file specified
# in the global variable ${CFG_FILE}.
start_kea() {
    local bin="${1-}"
    if [ -z "${bin}" ]; then
        test_lib_error "binary name must be specified for start_kea"
        clean_exit 1
    fi
    printf "Running command %s.\n" "\"${bin} -X -c ${CFG_FILE}\""
    "${bin}" -X -c "${CFG_FILE}" &
}

# Waits with timeout for Kea to start.
# This function repeatedly checks if the Kea log file has been created
# and is non-empty. If it is, the function assumes that Kea has started.
# It doesn't check the contents of the log file though.
# If the log file doesn't exist the function sleeps for a second and
# checks again. This is repeated until timeout is reached or non-empty
# log file is found. If timeout is reached, the function reports an
# error.
# Return value:
#    _WAIT_FOR_KEA: 0 if Kea hasn't started, 1 otherwise
wait_for_kea() {
    local timeout="${1-}"   # Desired timeout in seconds.
    if test -z "${timeout}"; then
        test_lib_error 'expected timeout parameter in wait_for_kea'
        clean_exit 1
    fi
    case ${timeout} in
        ''|*[!0-9]*)
            test_lib_error "argument passed to wait_for_kea must be a number"
            clean_exit 1 ;;
    esac
    local loops=0 # Loops counter
    _WAIT_FOR_KEA=0
    test_lib_info "wait_for_kea " "skip-new-line"
    while [ ! -s "${LOG_FILE}" ] && [ "${loops}" -le "${timeout}" ]; do
        printf "."
        sleep 1
        loops=$(( loops + 1 ))
    done
    printf '\n'
    if [ "${loops}" -le "${timeout}" ]; then
        _WAIT_FOR_KEA=1
    fi
}

# Waits for a specific message to occur in the Kea log file.
# This function is called when the test expects specific message
# to show up in the log file as a result of some action that has
# been taken. Typically, the test expects that the message
# is logged when the SIGHUP or SIGTERM signal has been sent to the
# Kea process.
# This function waits a specified number of seconds for the number
# of message occurrences to show up. If the expected number of
# message doesn't occur, the error status is returned.
# Return value:
#    _WAIT_FOR_MESSAGE: 0 if the message hasn't occurred, 1 otherwise.
wait_for_message() {
    local timeout="${1-}"      # Expected timeout value in seconds.
    local message="${2-}"      # Expected message id.
    local occurrences="${3-}"  # Number of expected occurrences.

    # Validate timeout
    case ${timeout} in
        ''|*[!0-9]*)
            test_lib_error "argument timeout passed to wait_for_message must \
be a number"
        clean_exit 1 ;;
    esac

    # Validate message
    if [ -z "${message}" ]; then
        test_lib_error "message id is a required argument for wait_for_message"
        clean_exit 1
    fi

    # Validate occurrences
    case ${occurrences} in
        ''|*[!0-9]*)
            test_lib_error "argument occurrences passed to wait_for_message \
must be a number"
        clean_exit 1 ;;
    esac

    local loops=0          # Number of loops performed so far.
    _WAIT_FOR_MESSAGE=0
    test_lib_info "wait_for_message ${message}: " "skip-new-line"
    # Check if log file exists and if we reached timeout.
    while [ "${loops}" -le "${timeout}" ]; do
        printf "."
        # Check if the message has been logged.
        get_log_messages "${message}"
        if [ "${_GET_LOG_MESSAGES}" -ge "${occurrences}" ]; then
            printf '\n'
            _WAIT_FOR_MESSAGE=1
            return
        fi
        # Message not recorded. Keep going.
        sleep 1
        loops=$(( loops + 1 ))
    done
    printf '\n'
    # Timeout.
}

# Waits for server to be down.
# Return value:
#    _WAIT_FOR_SERVER_DOWN: 1 if server is down, 0 if timeout occurred and the
#                             server is still running.
wait_for_server_down() {
    local timeout="${1-}"    # Timeout specified in seconds.
    local proc_name="${2-}"  # Server process name.
    if test -z "${proc_name}"; then
        test_lib_error 'expected process name parameter in wait_for_server_down'
        clean_exit 1
    fi

    case ${timeout} in
        ''|*[!0-9]*)
            test_lib_error "argument passed to wait_for_server_down must be a number"
            clean_exit 1 ;;
    esac
    local loops=0 # Loops counter
    _WAIT_FOR_SERVER_DOWN=0
    test_lib_info "wait_for_server_down ${proc_name}: " "skip-new-line"
    while [ "${loops}" -le "${timeout}" ]; do
        printf "."
        get_pid "${proc_name}"
        if [ "${_GET_PIDS_NUM}" -eq 0 ]; then
            printf '\n'
            _WAIT_FOR_SERVER_DOWN=1
            return
        fi
        sleep 1
        loops=$(( loops + 1 ))
    done
    printf '\n'
}

# Sends specified signal to the Kea process.
send_signal() {
    local sig="${1-}"           # Signal number.
    local proc_name="${2-}"     # Process name

    # Validate signal
    case ${sig} in
        ''|*[!0-9]*)
            test_lib_error "signal number passed to send_signal \
must be a number"
        clean_exit 1 ;;
    esac
    # Validate process name
    if [ -z "${proc_name}" ]; then
        test_lib_error "send_signal requires process name be passed as argument"
        clean_exit 1
    fi
    # Get Kea pid.
    get_pid "${proc_name}"
    if [ "${_GET_PIDS_NUM}" -ne 1 ]; then
        printf "ERROR: expected one Kea process to be started.\
 Found %d processes started.\n" ${_GET_PIDS_NUM}
        clean_exit 1
    fi
    printf "Sending signal %s to Kea process (pid=%s).\n" "${sig}" "${_GET_PID}"
    # Actually send a signal.
    kill "-${sig}" "${_GET_PID}"
}

# Verifies that a server is up running by its PID file
# The PID file is constructed from the given config file name and
# binary name.  If it exists and the PID it contains refers to a
# live process it sets _SERVER_PID_FILE and _SERVER_PID to the
# corresponding values.  Otherwise, it emits an error and exits.
verify_server_pid() {
    local bin_name="${1-}" # binary name of the server
    local cfg_file="${2-}" # config file name

    # We will construct the PID file name based on the server config
    # and binary name
    if [ -z "${bin_name}" ]; then
        test_lib_error "verify_server_pid requires binary name"
        clean_exit 1
    fi

    if [ -z "${cfg_file}" ]; then
        test_lib_error "verify_server_pid requires config file name"
        clean_exit 1
    fi

    # Only the file name portion of the config file is used, try and
    # extract it. NOTE if this "algorithm" changes this code will need
    # to be updated.
    fname=$(basename "${cfg_file}")
    fname=$(echo "${fname}" | cut -f1 -d'.')

    if [ -z "${fname}" ]; then
        test_lib_error "verify_server_pid could not extract config name"
        clean_exit 1
    fi

    # Now we can build the name:
    pid_file="${KEA_PIDFILE_DIR}/${fname}.${bin_name}.pid"

    if [ ! -e "${pid_file}" ]; then
        printf "ERROR: PID file:[%s] does not exist\n" "${pid_file}"
        clean_exit 1
    fi

    # File exists, does its PID point to a live process?
    pid=$(cat "${pid_file}" 2> /dev/null || true)
    if ! kill -0 "${pid}"; then
        printf "ERROR: PID file:[%s] exists but PID:[%d] does not\n" \
               "${pid_file}" "${pid}"
        clean_exit 1
    fi

    # Make the values accessible to the caller
    _SERVER_PID="${pid}"
    _SERVER_PID_FILE="${pid_file}"
}

# This test verifies that the binary is reporting its version properly.
version_test() {
    test_name=${1}      # Test name
    long_version=${2-}  # Test long version?

    # Log the start of the test and print test name.
    test_start "${test_name}"

    # If set to anything other than empty string, reset it to the long version
    # parameter.
    if test -n "${long_version}"; then
        long_version='--version'
    fi

    # Keep ${long_version} unquoted so that it is not included as an empty
    # string if not given as argument.
    for v in -v ${long_version}; do
        run_command \
            "${bin_path}/${bin}" "${v}"

        if test 0 -ne "${EXIT_CODE}"; then
            printf 'ERROR: Expected exit code 0, got "%s" when calling "%s"\n' \
                "${EXIT_CODE}" "${bin} -V"
            clean_exit 1
        fi

        if test "${OUTPUT}" != "${VERSION}"; then
            printf 'ERROR: Expected version "%s", got "%s" when calling "%s"\n' \
                "${VERSION}" "${OUTPUT}" "${bin} ${v}"
            test_finish 1
        fi
    done

    # Check -V.
    run_command \
        "${bin_path}/${bin}" -V
    if test 0 -ne "${EXIT_CODE}"; then
        printf 'ERROR: Expected exit code 0, got "%s" when calling "%s"\n' \
            "${EXIT_CODE}" "${bin} -V"
        clean_exit 1
    fi
    if test "${EXTENDED_VERSION}" != "$(echo "${OUTPUT}" | head -n 1)"; then
        printf 'ERROR: Expected version "%s", got "%s" when calling "%s"\n' \
            "${EXTENDED_VERSION}" "${OUTPUT}" "${bin} -V"
        clean_exit 1
    fi

    test_finish 0
}

# This test verifies that the server is using logger variable
# KEA_LOCKFILE_DIR properly (it should be used to point out to the directory,
# where lockfile should be created. Also, "none" value means to not create
# the lockfile at all).
logger_vars_test() {
    test_name=${1}  # Test name

    # Log the start of the test and print test name.
    test_start "${test_name}"

    # Create bogus configuration file. We don't really want the server to start,
    # just want it to log something and die. Empty config is an easy way to
    # enforce that behavior.
    create_config "{ }"
    printf "Please ignore any config error messages.\n"

    # Remember old KEA_LOCKFILE_DIR
    KEA_LOCKFILE_DIR_OLD=${KEA_LOCKFILE_DIR}

    # Set lockfile directory to current directory.
    KEA_LOCKFILE_DIR=.

    # Start Kea.
    start_kea "${bin_path}/${bin}"

    # Wait for Kea to process the invalid configuration and die.
    sleep 1

    # Check if it is still running. It should have terminated.
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 0 ]; then
        printf 'ERROR: expected Kea process to not start. '
        printf 'Found %d processes running.\n' "${_GET_PIDS_NUM}"

        # Revert to the old KEA_LOCKFILE_DIR value
        KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD}
        clean_exit 1
    fi

    if [ ! -f "./logger_lockfile" ]; then
        printf 'ERROR: Expect %s to create logger_lockfile in the ' "${bin}"
        printf 'current directory, but no such file exists.\n'

        # Revert to the old KEA_LOCKFILE_DIR value
        KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR__OLD}
        clean_exit 1
    fi

    # Remove the lock file
    rm -f ./logger_lockfile

    # Tell Kea to NOT create logfiles at all
    KEA_LOCKFILE_DIR="none"

    # Start Kea.
    start_kea "${bin_path}/${bin}"

    # Wait for Kea to process the invalid configuration and die.
    sleep 1

    # Check if it is still running. It should have terminated.
    get_pid "${bin}"
    if [ "${_GET_PIDS_NUM}" -ne 0 ]; then
        printf 'ERROR: expected Kea process to not start. '
        printf 'Found %d processes running.\n' "${_GET_PIDS_NUM}"

        # Revert to the old KEA_LOCKFILE_DIR value
        KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD}

        clean_exit 1
    fi

    if [ -f "./logger_lockfile" ]; then
        printf 'ERROR: Expect %s to NOT create logger_lockfile in the ' "${bin}"
        printf 'current directory, but the file exists.\n'

        # Revert to the old KEA_LOCKFILE_DIR value
        KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD}

        clean_exit 1
    fi

    # Revert to the old KEA_LOCKFILE_DIR value
    printf 'Reverting KEA_LOCKFILE_DIR to %s\n' "${KEA_LOCKFILE_DIR_OLD}"
    KEA_LOCKFILE_DIR=${KEA_LOCKFILE_DIR_OLD}

    test_finish 0
}

# This test verifies server PID file management
# 1. It verifies that upon startup, the server creates a PID file
# 2. It verifies the an attempt to start a second instance fails
# due to pre-existing PID File/PID detection
server_pid_file_test() {
    local server_cfg="${1}"
    local log_id="${2}"

    # Log the start of the test and print test name.
    test_start "${bin}.server_pid_file_test"
    # Create new configuration file.
    create_config "${CONFIG}"
    # Instruct server to log to the specific file.
    set_logger
    # Start server
    start_kea "${bin_path}/${bin}"
    # Wait up to 20s for server to start.
    wait_for_kea 20
    if [ "${_WAIT_FOR_KEA}" -eq 0 ]; then
        printf 'ERROR: timeout waiting for %s to start.\n' "${bin}"
        clean_exit 1
    fi

    # Verify server is still running
    verify_server_pid "${bin}" "${CFG_FILE}"

    printf 'PID file is [%s], PID is [%d]\n' "${_SERVER_PID_FILE}" "${_SERVER_PID}"

    # Now try to start a second one
    start_kea "${bin_path}/${bin}"

    wait_for_message 10 "${log_id}" 1
    if [ "${_WAIT_FOR_MESSAGE}" -eq 0 ]; then
        printf 'ERROR: Second %s instance started? ' "${bin}"
        printf 'PID conflict not reported.\n'
        clean_exit 1
    fi

    # Verify server is still running
    verify_server_pid "${bin}" "${CFG_FILE}"

    # All ok. Shut down the server and exit.
    test_finish 0
}

# This test verifies that passwords are redacted in logs.
# This function takes 3 parameters:
# - test_name
# - config - string with a content of the config (will be written to a file)
# - expected_code - expected exit code returned by kea (0 - success, 1 - failure)
password_redact_test() {
    local test_name="${1}"
    local config="${2}"
    local expected_code="${3}"

    # Log the start of the test and print test name.
    test_start "${test_name}"
    # Create correct configuration file.
    create_config "${config}"
    # Instruct Control Agent to log to the specific file.
    set_logger
    # Check it
    printf "Running command %s.\n" "\"${bin_path}/${bin} -X -d -t ${CFG_FILE}\""
    run_command \
        "${bin_path}/${bin}" -X -d -t "${CFG_FILE}"
    if [ "${EXIT_CODE}" -ne "${expected_code}" ]; then
        printf 'ERROR: expected exit code %s, got %s\n' "${expected_code}" "${EXIT_CODE}"
        clean_exit 1
    fi
    if grep -q 'sensitive' "${LOG_FILE}"; then
        printf "ERROR: sensitive is present in logs\n"
        clean_exit 1
    fi
    if ! grep -q 'superadmin' "${LOG_FILE}"; then
        printf "ERROR: superadmin is not present in logs\n"
        clean_exit 1
    fi
    test_finish 0
}

# kea-dhcp[46] configuration with a password
# used for redact tests:
# - sensitive should be hidden
# - superadmin should be visible
kea_dhcp_config() {
    printf '
{
  "Dhcp%s": {
    "config-control": {
      "config-databases": [
        {
          "password": "sensitive",
          "type": "mysql",
          "user": "keatest"
        }
      ]
    },
    "hooks-libraries": [
      {
        "library": "@abs_top_builddir@/src/bin/dhcp%s/tests/libco1.so",
        "parameters": {
          "password": "sensitive",
          "user": "keatest",
          "nested-map": {
            "password": "sensitive",
            "user": "keatest"
          }
        }
      }
    ],
    "hosts-database": {
      "password": "sensitive",
      "type": "mysql",
      "user": "keatest"
    },
    "lease-database": {
      "password": "sensitive",
      "type": "mysql",
      "user": "keatest"
    },
    "user-context": {
      "password": "superadmin",
      "secret": "superadmin",
      "shared-info": {
        "password": "superadmin",
        "secret": "superadmin"
      }
    }
  }
}
' "${1}" "${1}"
}
